﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using System.Windows.Input;

using YourLastOptionsDialog.Common;
using YourLastOptionsDialog.Controls;
using YourLastOptionsDialog.Core.Attributes;

namespace YourLastOptionsDialog.ViewModels.Options
{
    /// <summary>
    /// Takes a property info from an arbitrary object and checks whether it has any option related attributes applied.
    /// Creates option instances from these properties, using the parameters configured by the attributes.
    /// </summary>
    internal class OptionsFactory
    {
        private readonly Dictionary<Type, Type> _integralTypeToOption = new Dictionary<Type, Type>
                                                                            {
                                                                                {
                                                                                    typeof(byte), typeof(IntegralOption<byte>)
                                                                                }, 
                                                                                {
                                                                                    typeof(sbyte), typeof(IntegralOption<sbyte>)
                                                                                }, 
                                                                                {
                                                                                    typeof(short), typeof(IntegralOption<short>)
                                                                                }, 
                                                                                {
                                                                                    typeof(ushort), typeof(IntegralOption<ushort>)
                                                                                }, 
                                                                                {
                                                                                    typeof(int), typeof(IntegralOption<int>)
                                                                                }, 
                                                                                {
                                                                                    typeof(uint), typeof(IntegralOption<uint>)
                                                                                }, 
                                                                                {
                                                                                    typeof(long), typeof(IntegralOption<long>)
                                                                                }, 
                                                                                {
                                                                                    typeof(ulong), typeof(IntegralOption<ulong>)
                                                                                }
                                                                            };

        private readonly Dictionary<Type, Type> _floatTypeToOption = new Dictionary<Type, Type>
                                                                         {
                                                                             {
                                                                                 typeof(float), typeof(FloatOption<float>)
                                                                             }, 
                                                                             {
                                                                                 typeof(double), typeof(FloatOption<double>)
                                                                             }, 
                                                                             {
                                                                                 typeof(decimal), typeof(FloatOption<decimal>)
                                                                             }
                                                                         };

        /// <summary>
        /// Creates an option for a given property of an arbitrary object instance.
        /// </summary>
        /// <param name="property">The property info to process.</param>
        /// <param name="instance">The instance the property can be found on.</param>
        /// <returns>An instance of a class derived from <see cref="Option"/>, or <c>null</c> if the property type is not supported or errors occurred during creation.</returns>
        public Option CreateOptionFor(PropertyInfo property, object instance)
        {
            // should this property be ignored?
            var optionIgnoreAttribute = GetOptionIgnoreAttribute(property);
            if (optionIgnoreAttribute != null)
            {
                return null;
            }

            // check to see if someone's trying to extend our functionality
            var extender = OptionsService.Current.OptionsFactoryExtender;
            if (extender != null)
            {
                Type optionType = null;
                Type optionAttributeType = null;
                try
                {
                    extender.DetermineTypesFor(property, ref optionType, ref optionAttributeType);
                }
                catch (Exception)
                {
                    // we're not responsible for our client's faults
                }

                // is the extender capable of handling this type?
                if (optionType != null)
                {
                    // make sure we do have an attribute type
                    if (optionAttributeType == null)
                    {
                        optionAttributeType = typeof(OptionAttribute);
                    }

                    // do some validation
                    var typesAreValid = true;
                    if (optionAttributeType != typeof(OptionAttribute) && !optionAttributeType.IsSubclassOf(typeof(OptionAttribute)))
                    {
                        typesAreValid = false;
                    }

                    if (!optionType.IsSubclassOf(typeof(Option)))
                    {
                        typesAreValid = false;
                    }

                    if (typesAreValid)
                    {
                        // create the option
                        var option = (Option)Activator.CreateInstance(optionType);
                        var optionAttribute = GetOptionAttribute(property, optionAttributeType);
                        SetBasicOptionProperties<object>(option, property, instance, optionAttribute);

                        // let the extender add its info
                        try
                        {
                            extender.SetCustomValues(property, option, optionAttribute, instance);
                        }
                        catch (Exception)
                        {
                            // if an error happens now, we ignore this property 
                            // (we cannot throw, because all this happens on a background thread, so we'd only produce unhandled exceptions)
                            return null;
                        }

                        return option;
                    }
                }
            }

            // create a suitable option type
            var propertyType = property.PropertyType;
            if (propertyType == typeof(bool))
            {
                return CreateBooleanOption(property, instance);
            }
            else if (propertyType == typeof(DateTime))
            {
                return CreateDateTimeOption(property, instance);
            }
            else if (_integralTypeToOption.ContainsKey(propertyType))
            {
                return CreateIntegralOption(property, instance);
            }
            else if (_floatTypeToOption.ContainsKey(propertyType))
            {
                return CreateFloatOption(property, instance);
            }
            else if (propertyType == typeof(string))
            {
                // check for password option attribute
                var passwordOptionAttribute = GetOptionAttribute<OptionPasswordAttribute>(property);

                if (passwordOptionAttribute != null)
                {
                    return CreatePasswordOption(property, instance, passwordOptionAttribute);
                }
                else
                {
                    return CreateStringOption(property, instance);
                }
            }
            else if (propertyType.IsEnum)
            {
                return CreateEnumOption(property, instance);
            }

            return null;
        }

        #region Creation methods of actual options

        private static Option CreateBooleanOption(PropertyInfo property, object instance)
        {
            var booleanOptionAttribute = GetOptionAttribute<OptionBooleanAttribute>(property);

            var option = new BooleanOption();
            SetBasicOptionProperties<bool>(option, property, instance, booleanOptionAttribute);

            // set confirmation properties
            option.UserMustConfirmActivation = booleanOptionAttribute != null && booleanOptionAttribute.UserMustConfirmActivation;
            option.UserMustConfirmDeactivation = booleanOptionAttribute != null && booleanOptionAttribute.UserMustConfirmDeactivation;
            option.ActivationPrompt = booleanOptionAttribute != null
                                          ? booleanOptionAttribute.Format(booleanOptionAttribute.ActivationPrompt)
                                          : string.Format(CultureInfo.CurrentCulture, "Do you really want to activate option '{0}'?", option.DisplayName);
            option.DeactivationPrompt = booleanOptionAttribute != null
                                            ? booleanOptionAttribute.Format(booleanOptionAttribute.DeactivationPrompt)
                                            : string.Format(CultureInfo.CurrentCulture, "Do you really want to deactivate option '{0}'?", option.DisplayName);

            return option;
        }

        private static Option CreateDateTimeOption(PropertyInfo property, object instance)
        {
            var dateTimeOptionAttribute = GetOptionAttribute<OptionDateTimeAttribute>(property);

            var option = new DateTimeOption();
            SetBasicOptionProperties<DateTime>(option, property, instance, dateTimeOptionAttribute);

            option.Mode = dateTimeOptionAttribute != null ? dateTimeOptionAttribute.Mode : DateTimeMode.Date;

            return option;
        }

        private Option CreateIntegralOption(PropertyInfo property, object instance)
        {
            var integralOptionAttribute = GetOptionAttribute<OptionIntegralAttribute>(property);

            var optionType = _integralTypeToOption[property.PropertyType];
            var option = (IntegralOptionBase)Activator.CreateInstance(optionType);

            // we use 'object' here to satisfy all possible integral types 
            // => this is converted to the real type in the generic implementation later
            SetBasicOptionProperties<object>(option, property, instance, integralOptionAttribute);

            // set min/max
            option.Minimum = integralOptionAttribute != null ? integralOptionAttribute.Minimum : long.MinValue;
            option.Maximum = integralOptionAttribute != null ? integralOptionAttribute.Maximum : long.MaxValue;
            option.ValidationErrorMessage = integralOptionAttribute != null ? integralOptionAttribute.Format(integralOptionAttribute.ValidationErrorMessage) : "Please enter a valid number.";

            return option;
        }

        private Option CreateFloatOption(PropertyInfo property, object instance)
        {
            var floatOptionAttribute = GetOptionAttribute<OptionFloatAttribute>(property);

            var optionType = _floatTypeToOption[property.PropertyType];
            var option = (FloatOptionBase)Activator.CreateInstance(optionType);

            // we use 'object' here to satisfy all possible float types 
            // => this is converted to the real type in the generic implementation later
            SetBasicOptionProperties<object>(option, property, instance, floatOptionAttribute);

            // set min/max
            option.Minimum = floatOptionAttribute != null ? floatOptionAttribute.Minimum : double.MinValue;
            option.Maximum = floatOptionAttribute != null ? floatOptionAttribute.Maximum : double.MaxValue;
            option.ValidationErrorMessage = floatOptionAttribute != null ? floatOptionAttribute.Format(floatOptionAttribute.ValidationErrorMessage) : "Please enter a valid number.";

            return option;
        }

        private static Option CreateStringOption(PropertyInfo property, object instance)
        {
            var stringOptionAttribute = GetOptionAttribute<OptionStringAttribute>(property);

            var option = new StringOption();
            SetBasicOptionProperties<string>(option, property, instance, stringOptionAttribute);

            // set all specific values
            SetStringOptionProperties(option, stringOptionAttribute);
            
            return option;
        }

        private static Option CreatePasswordOption(PropertyInfo property, object instance, OptionPasswordAttribute optionPasswordAttribute)
        {
            var option = new PasswordOption();
            SetBasicOptionProperties<string>(option, property, instance, optionPasswordAttribute);
            
            // set all string option values
            SetStringOptionProperties(option, optionPasswordAttribute);

            // set all specific values
            option.ConfirmationPassword = option.TextValue;
            option.ShowConfirmation = optionPasswordAttribute.ShowConfirmation;
            option.ConfirmationDescription = GetFormattedText(property, optionPasswordAttribute, optionPasswordAttribute.ConfirmationDescription);
            option.PasswordsDoNotMatchErrorMessage = GetFormattedText(property, optionPasswordAttribute, optionPasswordAttribute.PasswordsDoNotMatchErrorMessage);

            return option;
        }

        private static void SetStringOptionProperties(StringOption option, OptionStringAttribute stringOptionAttribute)
        {
            option.MinLength = stringOptionAttribute != null ? stringOptionAttribute.MinLength : 0;
            option.MaxLength = stringOptionAttribute != null ? stringOptionAttribute.MaxLength : int.MaxValue;
            option.InputScopeNameValue = stringOptionAttribute != null ? stringOptionAttribute.InputScopeNameValue : InputScopeNameValue.Default;
            option.ValidationErrorMessage = stringOptionAttribute != null ? stringOptionAttribute.Format(stringOptionAttribute.ValidationErrorMessage) : "Please enter a valid text.";

            // should we also "format" the regex, so the user has options to localize the pattern?
            option.RegularExpression = stringOptionAttribute != null ? stringOptionAttribute.RegularExpression : "^.*$";

            // only explicitly set this if we have a value (or else we'd overwrite the internally created validator)
            if (stringOptionAttribute != null && stringOptionAttribute.ValidatorType != null)
            {
                var validator = Activator.CreateInstance(stringOptionAttribute.ValidatorType) as IValidator;

                // ignore bogus settings/classes that do not implement IValidator
                if (validator != null)
                {
                    option.Validator = validator;
                }
            }
        }

        private static Option CreateEnumOption(PropertyInfo property, object instance)
        {
            var enumOptionAttribute = GetOptionAttribute<OptionEnumAttribute>(property);

            var option = new EnumOption();
            SetBasicOptionProperties<object>(option, property, instance, enumOptionAttribute);

            // get all the possible enum values
            var enumValues = new List<EnumValue>();
            foreach (var fieldInfo in property.PropertyType.GetFields(BindingFlags.Public | BindingFlags.Static))
            {
                var enumValue = new EnumValue();
                enumValue.OriginalValue = fieldInfo.GetValue(null);

                // for the display name, we go through more efforts
                var descriptionAttributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
                if (descriptionAttributes.Length > 0)
                {
                    enumValue.DisplayName = descriptionAttributes[0].Description;
                }
                else
                {
                    var rawStringRepresentation = enumValue.OriginalValue.ToString();
                    enumValue.DisplayName = StringFormatter.DisplayNameFromCamelCase(rawStringRepresentation);
                }

                enumValues.Add(enumValue);
            }

            option.SetLookupValues(enumValues);

            return option;
        }

        #endregion

        #region Helper methods to get attributes and base property values

        private static void SetBasicOptionProperties<T>(Option option, PropertyInfo property, object instance, OptionAttribute optionOptionAttribute)
        {
            option.DisplayName = GetPropertyDisplayName(property, optionOptionAttribute);
            option.Description = GetPropertyDescription(optionOptionAttribute);
            option.GroupName = GetPropertyGroupName(optionOptionAttribute);
            option.DisplayOrder = optionOptionAttribute != null ? optionOptionAttribute.DisplayOrder : 0;
            option.GetAccessor = () => (T)property.GetValue(instance, null);
            option.SetAccessor = o => property.SetValue(instance, o, null);
        }

        private static T GetOptionAttribute<T>(PropertyInfo property)
            where T : OptionAttribute
        {
            var result = property.GetCustomAttributes(typeof(T), false); // the second parameter is ignored
            if (result.Length > 0)
            {
                return (T)result[0];
            }

            return null;
        }

        private static OptionAttribute GetOptionAttribute(PropertyInfo property, Type attributeType)
        {
            var result = property.GetCustomAttributes(attributeType, false); // the second parameter is ignored
            if (result.Length > 0)
            {
                return (OptionAttribute)result[0];
            }

            return null;
        }

        private static OptionIgnoreAttribute GetOptionIgnoreAttribute(PropertyInfo property)
        {
            var result = property.GetCustomAttributes(typeof(OptionIgnoreAttribute), false); // the second parameter is ignored
            if (result.Length > 0)
            {
                return (OptionIgnoreAttribute)result[0];
            }

            return null;
        }

        private static string GetPropertyDisplayName(PropertyInfo property, OptionAttribute optionOptionAttribute)
        {
            if (optionOptionAttribute != null && !string.IsNullOrEmpty(optionOptionAttribute.DisplayName))
            {
                return optionOptionAttribute.Format(optionOptionAttribute.DisplayName);
            }

            return StringFormatter.DisplayNameFromCamelCase(property.Name);
        }

        private static string GetFormattedText(PropertyInfo property, OptionAttribute optionAttribute, string source)
        {
            if (optionAttribute != null && !string.IsNullOrEmpty(source))
            {
                return optionAttribute.Format(source);
            }

            return StringFormatter.DisplayNameFromCamelCase(property.Name);
        }

        private static string GetPropertyDescription(OptionAttribute optionOptionAttribute)
        {
            if (optionOptionAttribute != null && !string.IsNullOrEmpty(optionOptionAttribute.Description))
            {
                return optionOptionAttribute.Format(optionOptionAttribute.Description);
            }

            return null;
        }

        private static string GetPropertyGroupName(OptionAttribute optionOptionAttribute)
        {
            if (optionOptionAttribute != null && !string.IsNullOrEmpty(optionOptionAttribute.GroupName))
            {
                return optionOptionAttribute.Format(optionOptionAttribute.GroupName);
            }

            return OptionsService.Current.Configuration.DefaultGroupName;
        }

        #endregion
    }
}